How to Automatically Apply Shadow Copying Settings When Publishing an ASP.NET Core Website
I first heard the term "Shadow Copying" from a colleague last year. Shadow Copying is a mechanism that copies application files to a temporary location for execution, rather than running them directly from the original deployment directory. This solves a common issue with ASP.NET Core at runtime: when a web application is running, the associated DLLs are locked, making it impossible to update files directly without stopping the application pool.
Initially, I referred to Will 保哥's article "How to Enable Shadow-copying for ASP.NET Core 6.0 Deployed to IIS" and found that it required manual modification of web.config. However, I have always adhered to the principle that "the build process should produce the complete configuration," and I did not want to manually adjust the configuration file every time, so I did not adopt this mechanism at first.
Later, my approach was to manually modify the web.config to add Shadow Copying functionality during the first deployment and leave the configuration file unchanged for subsequent updates. However, this approach is still not ideal because if there is any negligence during the handover or deployment process, and the original web.config is accidentally overwritten, the Shadow Copying mechanism will fail. This method is particularly unsuitable for CI/CD workflows, as modifying web.config in automated scripts is difficult and prone to errors.
Recently, I discovered a better solution: if you pre-place a web.config file in the root of your Web project, the published web.config will be generated based on it. This makes automated deployment much simpler.
Here is the web.config after a standard publish:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<location path="." inheritInChildApplications="false">
<system.webServer>
<handlers>
<add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModuleV2" resourceType="Unspecified" />
</handlers>
<aspNetCore processPath="dotnet" arguments=".\WebApi.dll" stdoutLogEnabled="false" stdoutLogFile=".\logs\stdout" hostingModel="inprocess" />
</system.webServer>
</location>
</configuration>2
3
4
5
6
7
8
9
10
11
If you add the following code to your project's publish profile (.pubxml), you can set the application's execution environment: (For specific usage, please refer to my previous article "A Brief Discussion on Environment Name Settings and Applications in ASP.NET Core"):
<EnvironmentName>Staging</EnvironmentName>The generated web.config will automatically include the environment variable settings:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<location path="." inheritInChildApplications="false">
<system.webServer>
<handlers>
<add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModuleV2" resourceType="Unspecified" />
</handlers>
<aspNetCore processPath="dotnet" arguments=".\WebApi.dll" stdoutLogEnabled="false" stdoutLogFile=".\logs\stdout" hostingModel="inprocess">
<!--Add environment variable settings for environment name-->
<environmentVariables>
<environmentVariable name="ASPNETCORE_ENVIRONMENT" value="Staging" />
</environmentVariables>
</aspNetCore>
</system.webServer>
</location>
</configuration>2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Now, if I pre-create a web.config file containing Shadow Copying settings in the root of the ASP.NET Core Web project:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<location path="." inheritInChildApplications="false">
<system.webServer>
<handlers>
<add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModuleV2" resourceType="Unspecified" />
</handlers>
<aspNetCore processPath="dotnet" arguments=".\WebApi.dll" stdoutLogEnabled="false" stdoutLogFile=".\logs\stdout" hostingModel="inprocess">
<!--Add Shadow Copying related settings-->
<handlerSettings>
<handlerSetting name="enableShadowCopy" value="true" />
<!--Specify the storage path for Shadow Copying files-->
<handlerSetting name="shadowCopyDirectory" value="../ShadowCopy/" />
</handlerSettings>
</aspNetCore>
</system.webServer>
</location>
</configuration>2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
The final generated web.config will merge both configurations, containing both the Shadow Copying functionality and the environment variables:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<location path="." inheritInChildApplications="false">
<system.webServer>
<handlers>
<add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModuleV2" resourceType="Unspecified" />
</handlers>
<aspNetCore processPath="dotnet" arguments=".\WebApi.dll" stdoutLogEnabled="false" stdoutLogFile=".\logs\stdout" hostingModel="inprocess">
<handlerSettings>
<handlerSetting name="enableShadowCopy" value="true" />
<handlerSetting name="shadowCopyDirectory" value="../ShadowCopy/" />
</handlerSettings>
<environmentVariables>
<environmentVariable name="ASPNETCORE_ENVIRONMENT" value="Staging" />
</environmentVariables>
</aspNetCore>
</system.webServer>
</location>
</configuration>2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
This way, in a CI/CD process, you only need to use the following command to publish an application with complete settings:
dotnet publish -c Release -p:EnvironmentName=StagingNote that the Shadow Copying settings in web.config vary by ASP.NET Core version. During the .NET 6 era, this feature was still in the experimental stage, so the parameter names differ from later versions. This setting is independent of the .NET version of the Web project itself and depends primarily on the version of the Hosting Bundle installed on the server. Here is how to use it for different versions:
- Using ASP.NET Core Hosting Bundle 7.0.0 or later: Use the
enableShadowCopyparameter. - Using ASP.NET Core Hosting Bundle 6.0.0: Use the
experimentalEnableShadowCopyparameter.
Regarding the Folder Management Mechanism of Shadow Copying
There is a common misconception in the community regarding how Shadow Copying manages the temporary folder (shadowCopyDirectory), assuming it helps "manage old versions" or accumulates versions and deletes them automatically. For example, when referring to some technical articles, you might see descriptions like this:
All files (including the wwwroot folder) will be copied to the directory specified by shadowCopyDirectory... The folder name is a sequence number, and each shadow copy will accumulate here; older versions will be deleted automatically, so the folder does not require manual maintenance.
However, if we look deeper into the operation mechanism from the ASP.NET Core source code (AspNetCoreModuleV2), we find that the official design intent differs from this "version management" perception. The comments in the APPLICATION_INFO::HandleShadowCopy(const ShimOptions& options, IHttpContext& pHttpContext) method state:
Other directories in the shadow copy directory will be cleaned up as well. Following the example, after '1' has been selected as the directory to use, we will start a thread that deletes all other folders in that directory.
This explanation indicates that once the system selects a new sequence number folder (e.g., 1) as the current execution path, it immediately starts a thread to delete all other folders in that directory.
This means that the behavior pattern of Shadow Copying is not to "maintain a list of versions" or "proactively manage old versions"; it is essentially inclined to keep only the one currently in use. If you find multiple folders coexisting in that directory, it is usually just because the cleanup thread has not finished processing, or because old files were locked and failed to be deleted, rather than the system intentionally keeping them for future use.
WARNING
After upgrading the .NET version, you must delete the Shadow Copying folder; otherwise, IIS will throw a 500.30 error. At this point, the Event Viewer will display the following error message:
textApplication '/LM/W3SVC/1/ROOT/{Application Pool}' with physical root '{Website Path}' failed to load coreclr. Exception message: Unexpected exception: directory_iterator::directory_iterator: The system cannot find the path specified.1
2When using Shadow Copying, be aware that you should avoid writing files or logs in the application directory, otherwise every file change may trigger the Shadow Copying mechanism, leading to unnecessary application restarts.
Change Log
- 2025-03-18 Initial document created.
- 2025-04-08 Added information on Shadow Copying anomalies caused by .NET version upgrades.
- 2025-06-27 Added precautions about avoiding writing files in the application directory when using Shadow Copying.
- 2026-01-22 Added common misconceptions and source code explanation regarding the Shadow Copying folder management mechanism.
